JavaScript Meta Programming Guide
Table of Contentsโ
- Introduction
- Proxies
- Handlers and Traps
- Revocable Proxy
- Reflection
- Practical Use Cases
- Best Practices
Introductionโ
The Proxy and Reflect objects allow you to intercept and define custom behavior for fundamental language operations (e.g., property lookup, assignment, enumeration, function invocation, etc.). With these two objects, you can program at the meta level of JavaScript.
Meta programming enables you to:
- Intercept and customize fundamental operations
- Create virtual properties
- Implement validation logic
- Add logging and debugging capabilities
- Build reactive data systems
Proxiesโ
What are Proxies?โ
Proxy objects allow you to intercept certain operations and implement custom behaviors. A proxy wraps another object and intercepts operations like reading/writing properties, function calls, and more.
Basic Exampleโ
Getting a property on an object:
const handler = {
get(target, name) {
return name in target ? target[name] : 42;
},
};
const p = new Proxy({}, handler);
p.a = 1;
console.log(p.a, p.b); // 1, 42
In this example:
- The Proxy object defines a target (an empty object) and a handler object
- The handler implements a
gettrap - When accessing an undefined property, instead of returning
undefined, it returns42
Terminologyโ
handlerโ
A placeholder object which contains traps.
const handler = {
get(target, prop) { /* ... */ },
set(target, prop, value) { /* ... */ }
};
trapsโ
The methods that provide property access. This is analogous to the concept of traps in operating systems.
// 'get' is a trap
get(target, property, receiver) {
// Custom behavior
}
targetโ
The object which the proxy virtualizes. It is often used as storage backend for the proxy.
const target = { message: "Hello" };
const proxy = new Proxy(target, handler);
invariantsโ
Semantics that remain unchanged when implementing custom operations. If you violate the invariants of a handler, a TypeError will be thrown.
Example invariant: A non-configurable property cannot be reported as non-existent by the get trap.
Handlers and Trapsโ
Available Trapsโ
The following table summarizes all available traps for Proxy objects:
| Handler / Trap | Interceptions |
|---|---|
| handler.getPrototypeOf() | Object.getPrototypeOf()Reflect.getPrototypeOf()__proto__Object.prototype.isPrototypeOf()instanceof |
| handler.setPrototypeOf() | Object.setPrototypeOf()Reflect.setPrototypeOf() |
| handler.isExtensible() | Object.isExtensible()Reflect.isExtensible() |
| handler.preventExtensions() | Object.preventExtensions()Reflect.preventExtensions() |
| handler.getOwnPropertyDescriptor() | Object.getOwnPropertyDescriptor()Reflect.getOwnPropertyDescriptor() |
| handler.defineProperty() | Object.defineProperty()Reflect.defineProperty() |
| handler.has() | Property query: foo in proxyInherited property query: foo in Object.create(proxy)Reflect.has() |
| handler.get() | Property access: proxy[foo], proxy.barInherited property access: Object.create(proxy)[foo]Reflect.get() |
| handler.set() | Property assignment: proxy[foo] = bar, proxy.foo = barInherited property assignment: Object.create(proxy)[foo] = barReflect.set() |
| handler.deleteProperty() | Property deletion: delete proxy[foo], delete proxy.fooReflect.deleteProperty() |
| handler.ownKeys() | Object.getOwnPropertyNames()Object.getOwnPropertySymbols()Object.keys()Reflect.ownKeys() |
| handler.apply() | proxy(..args)Function.prototype.apply()Function.prototype.call()Reflect.apply() |
| handler.construct() | new proxy(...args)Reflect.construct() |
Trap Examplesโ
get trapโ
const handler = {
get(target, prop, receiver) {
console.log(`Property ${prop} was accessed`);
return Reflect.get(target, prop, receiver);
}
};
const proxy = new Proxy({ name: "John" }, handler);
console.log(proxy.name); // Logs: "Property name was accessed", then "John"
set trapโ
const handler = {
set(target, prop, value, receiver) {
if (prop === 'age' && typeof value !== 'number') {
throw new TypeError('Age must be a number');
}
return Reflect.set(target, prop, value, receiver);
}
};
const proxy = new Proxy({}, handler);
proxy.age = 25; // Works
proxy.age = "25"; // Throws TypeError
has trapโ
const handler = {
has(target, prop) {
if (prop.startsWith('_')) {
return false; // Hide private properties
}
return Reflect.has(target, prop);
}
};
const proxy = new Proxy({ _secret: 'hidden', public: 'visible' }, handler);
console.log('public' in proxy); // true
console.log('_secret' in proxy); // false
deleteProperty trapโ
const handler = {
deleteProperty(target, prop) {
if (prop.startsWith('_')) {
throw new Error('Cannot delete private properties');
}
return Reflect.deleteProperty(target, prop);
}
};
const proxy = new Proxy({ _id: 1, name: 'John' }, handler);
delete proxy.name; // Works
delete proxy._id; // Throws Error
apply trapโ
const handler = {
apply(target, thisArg, argumentsList) {
console.log(`Function called with args: ${argumentsList}`);
return Reflect.apply(target, thisArg, argumentsList);
}
};
const sum = (a, b) => a + b;
const proxy = new Proxy(sum, handler);
proxy(2, 3); // Logs: "Function called with args: 2,3", Returns: 5
construct trapโ
const handler = {
construct(target, args, newTarget) {
console.log(`Constructor called with args: ${args}`);
return Reflect.construct(target, args, newTarget);
}
};
class Person {
constructor(name) {
this.name = name;
}
}
const ProxyPerson = new Proxy(Person, handler);
const john = new ProxyPerson('John'); // Logs: "Constructor called with args: John"
Revocable Proxyโ
The Proxy.revocable() method creates a revocable Proxy object. This means the proxy can be revoked via the revoke function, which switches the proxy off.
After revocation, any operation on the proxy leads to a TypeError.
const revocable = Proxy.revocable(
{},
{
get(target, name) {
return `[[${name}]]`;
},
},
);
const proxy = revocable.proxy;
console.log(proxy.foo); // "[[foo]]"
// Revoke the proxy
revocable.revoke();
console.log(proxy.foo); // TypeError: Cannot perform 'get' on a proxy that has been revoked
proxy.foo = 1; // TypeError: Cannot perform 'set' on a proxy that has been revoked
delete proxy.foo; // TypeError: Cannot perform 'deleteProperty' on a proxy that has been revoked
console.log(typeof proxy); // "object" - typeof doesn't trigger any trap
Use cases for revocable proxies:
- Temporary access control
- Resource management
- Security boundaries
- Time-limited permissions
Reflectionโ
What is Reflect?โ
Reflect is a built-in object that provides methods for interceptable JavaScript operations. The methods are the same as those of the proxy handler's traps.
Key characteristics:
Reflectis not a function object (cannot be called or constructed)- It's a plain object containing static methods
- Methods correspond one-to-one with proxy traps
- Helps with forwarding default operations from handler to target
Reflect Methodsโ
All Reflect methods correspond to proxy traps:
Reflect.get(target, propertyKey[, receiver])
Reflect.set(target, propertyKey, value[, receiver])
Reflect.has(target, propertyKey)
Reflect.deleteProperty(target, propertyKey)
Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(target, prototype)
Reflect.isExtensible(target)
Reflect.preventExtensions(target)
Reflect.getOwnPropertyDescriptor(target, propertyKey)
Reflect.defineProperty(target, propertyKey, attributes)
Reflect.ownKeys(target)
Reflect.apply(target, thisArgument, argumentsList)
Reflect.construct(target, argumentsList[, newTarget])
Benefits of Reflectโ
1. Functional approach to operatorsโ
With Reflect.has(), you get the in operator as a function:
Reflect.has(Object, "assign"); // true
// Instead of:
"assign" in Object; // true
2. Better apply() functionโ
Before Reflect:
Function.prototype.apply.call(Math.floor, undefined, [1.75]);
With Reflect:
Reflect.apply(Math.floor, undefined, [1.75]); // 1
Reflect.apply(String.fromCharCode, undefined, [104, 101, 108, 108, 111]);
// "hello"
Reflect.apply(RegExp.prototype.exec, /ab/, ["confabulation"]).index;
// 4
Reflect.apply("".charAt, "ponies", [3]);
// "i"
3. Boolean return values instead of exceptionsโ
With Object.defineProperty():
try {
Object.defineProperty(obj, 'prop', { value: 42 });
// Success
} catch (e) {
// Failed
}
With Reflect.defineProperty():
if (Reflect.defineProperty(obj, 'prop', { value: 42 })) {
// Success
} else {
// Failed
}
4. Proper this binding in proxy trapsโ
const handler = {
get(target, prop, receiver) {
// receiver ensures correct 'this' binding
return Reflect.get(target, prop, receiver);
}
};
Practical Use Casesโ
Validationโ
Validate object properties before setting them:
const validator = {
set(target, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('Age must be an integer');
}
if (value < 0 || value > 150) {
throw new RangeError('Age must be between 0 and 150');
}
}
return Reflect.set(target, prop, value);
}
};
const person = new Proxy({}, validator);
person.age = 25; // Works
person.age = -5; // Throws RangeError
person.age = "25"; // Throws TypeError
Default Valuesโ
Provide default values for missing properties:
const withDefaults = (target, defaults) => {
return new Proxy(target, {
get(obj, prop) {
return prop in obj ? obj[prop] : defaults[prop];
}
});
};
const settings = withDefaults(
{ theme: 'dark' },
{ theme: 'light', language: 'en', fontSize: 14 }
);
console.log(settings.theme); // 'dark'
console.log(settings.language); // 'en'
console.log(settings.fontSize); // 14
Logging and Debuggingโ
Track property access and modifications:
const createLogger = (target, name) => {
return new Proxy(target, {
get(obj, prop) {
console.log(`[${name}] Getting property "${prop}"`);
return Reflect.get(obj, prop);
},
set(obj, prop, value) {
console.log(`[${name}] Setting property "${prop}" to "${value}"`);
return Reflect.set(obj, prop, value);
}
});
};
const user = createLogger({ name: 'John' }, 'User');
user.name; // Logs: [User] Getting property "name"
user.age = 30; // Logs: [User] Setting property "age" to "30"
Data Bindingโ
Create reactive data with automatic updates:
const createReactive = (target, onChange) => {
return new Proxy(target, {
set(obj, prop, value) {
const oldValue = obj[prop];
const result = Reflect.set(obj, prop, value);
if (oldValue !== value) {
onChange(prop, oldValue, value);
}
return result;
}
});
};
const state = createReactive(
{ count: 0 },
(prop, oldVal, newVal) => {
console.log(`${prop} changed from ${oldVal} to ${newVal}`);
}
);
state.count = 1; // Logs: "count changed from 0 to 1"
state.count = 2; // Logs: "count changed from 1 to 2"
Negative Array Indicesโ
Access arrays with negative indices (like Python):
const createNegativeArray = (arr) => {
return new Proxy(arr, {
get(target, prop) {
const index = Number(prop);
if (Number.isInteger(index) && index < 0) {
return target[target.length + index];
}
return Reflect.get(target, prop);
}
});
};
const arr = createNegativeArray([1, 2, 3, 4, 5]);
console.log(arr[-1]); // 5
console.log(arr[-2]); // 4
Private Propertiesโ
Hide properties that start with underscore:
const hidePrivate = (target) => {
return new Proxy(target, {
has(obj, prop) {
if (typeof prop === 'string' && prop.startsWith('_')) {
return false;
}
return Reflect.has(obj, prop);
},
ownKeys(obj) {
return Reflect.ownKeys(obj).filter(
key => typeof key !== 'string' || !key.startsWith('_')
);
},
get(obj, prop) {
if (typeof prop === 'string' && prop.startsWith('_')) {
throw new Error(`Cannot access private property: ${prop}`);
}
return Reflect.get(obj, prop);
}
});
};
const obj = hidePrivate({ _secret: 'hidden', public: 'visible' });
console.log('public' in obj); // true
console.log('_secret' in obj); // false
console.log(Object.keys(obj)); // ['public']
Best Practicesโ
1. Always use Reflect in trapsโ
Always use Reflect methods to perform the default operation:
// Good
const handler = {
get(target, prop, receiver) {
// Custom logic here
return Reflect.get(target, prop, receiver);
}
};
// Bad - manual implementation is error-prone
const handler = {
get(target, prop) {
return target[prop]; // Doesn't handle receiver correctly
}
};
2. Return appropriate valuesโ
Ensure traps return the expected types:
// set trap must return boolean
set(target, prop, value) {
// ... validation logic
return Reflect.set(target, prop, value); // Returns boolean
}
// has trap must return boolean
has(target, prop) {
return Reflect.has(target, prop); // Returns boolean
}
3. Respect invariantsโ
Don't violate JavaScript's invariants or you'll get TypeErrors:
// Bad - violates invariant
const handler = {
getPrototypeOf(target) {
return null; // If target is non-extensible, must return actual prototype
}
};
4. Performance considerationsโ
Proxies add overhead. Use them judiciously:
// Don't wrap frequently accessed objects unnecessarily
// Only use proxies when you need interception
// Good - proxy for validation
const validatedUser = new Proxy(user, validator);
// Bad - unnecessary proxy
const simpleObject = new Proxy({}, {}); // Just use {}
5. Use revocable proxies for temporary accessโ
const { proxy, revoke } = Proxy.revocable(sensitiveData, handler);
// Give temporary access
temporaryFunction(proxy);
// Revoke access when done
revoke();
6. Document proxy behaviorโ
Make it clear when objects are proxies:
/**
* Creates a validated user object
* @returns {Proxy} A proxy that validates user properties
*/
function createUser(data) {
return new Proxy(data, validationHandler);
}